package com.bz.match;

import com.bz.match.utils.SnowFlakeIdGenerator;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author zhongyongbin
 * @since 2021/5/17
 */
public final class PrePlayMatchStrategy implements MatchStrategy {

    private static final String WAIT_ROOM_KEY = "match:wroom:{%d}";

    private static final String PRE_PLAY_ROOM_LIST_KEY = "match:proomlist:{%d}";

    private static final String GAME_ROOM_KEY = "match:groom:{%d}:%d";

    private static final String JOIN_WAIT_ROOM_SCRIPT_CONTENT = "local score = redis.call(\"ZSCORE\", KEYS[1], ARGV[1])\n" +
            "if score == nil then\n" +
            "    redis.call(\"ZADD\", KEYS[1], ARGV[2], ARGV[1])\n" +
            "    return true\n" +
            "end\n" +
            "return false";

    private static final String MATCH_SCRIPT_CONTENT = "local prePlayRoom = redis.call('LPOP', KEYS[2])\n" +
            "if prePlayRoom == nil then\n" +
            "    if redis.call('ZCARD', KEYS[1]) <  tonumber(ARGV[1]) then\n" +
            "        return nil\n" +
            "    end\n" +
            "    for i, v in ipairs(redis.call('ZPOPMAX', KEYS[1], tonumber(ARGV[1]))) then\n" +
            "        if i % 2 ~= 0 then\n" +
            "            redis.call('SADD', KEYS[3], v)\n" +
            "        end\n" +
            "    end\n" +
            "    return KEYS[3]\n" +
            "else then\n" +
            "    local cnt = tonumber(ARGV[1]) - redis.call('SCARD', prePlayRoom)\n" +
            "    local waitCnt = redis.call('ZCARD', KEYS[1])\n" +
            "    if waitCnt < cnt then\n" +
            "        cnt = waitCnt\n" +
            "    end\n" +
            "    if cnt == 0 then\n" +
            "        return nil\n" +
            "    end\n" +
            "    for i, v in ipairs(redis.call('ZPOPMIN', KEYS[1], cnt)) then\n" +
            "        if i % 2 ~= 0 then\n" +
            "            redis.call('SADD', KEYS[3], v)\n" +
            "        end\n" +
            "    end\n" +
            "    return prePlayRoom\n" +
            "end";

    private static final String PREPLAY_SCRIPT_CONTENT = "local rem = redis.call('ZREM', KEYS[1], ARGV[1])\n" +
            "if rem == 0 then\n" +
            "    return false\n" +
            "end\n" +
            "redis.call('RPUSH', KEYS[2], ARGV[1])\n" +
            "redis.call('SADD', KEYS[3], ARGV[2])\n" +
            "return true";

    private final StringRedisTemplate redisTemplate;

    private final DefaultRedisScript<Boolean> joinWaitRoomScript = new DefaultRedisScript<>(JOIN_WAIT_ROOM_SCRIPT_CONTENT, Boolean.class);

    private final DefaultRedisScript<Long> matchScript = new DefaultRedisScript<>(MATCH_SCRIPT_CONTENT, Long.class);

    private final DefaultRedisScript<Boolean> preplayScript = new DefaultRedisScript<>(PREPLAY_SCRIPT_CONTENT, Boolean.class);

    public PrePlayMatchStrategy(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public MatchResult match(int gameId, int playerCount) {
        long prepareRoomId = SnowFlakeIdGenerator.generateId();
        Long roomId = redisTemplate.execute(matchScript,
                Arrays.asList(getWaitRoomKey(gameId), getPrePlayRoomListKey(gameId), getGameRoomKey(gameId, prepareRoomId),
                        String.valueOf(playerCount)));
        boolean play = Objects.nonNull(roomId);
        if (play) {
            List<Long> players = redisTemplate.opsForSet().members(getGameRoomKey(gameId, roomId))
                    .stream()
                    .map(Long::parseLong)
                    .collect(Collectors.toList());
            return new MatchResult(true, new GameRoom(roomId, players));
        }
        return new MatchResult(false);
    }

    @Override
    public boolean joinWaitRoom(int gameId, long userId) {
        Boolean res = redisTemplate.execute(joinWaitRoomScript,
                Collections.singletonList(getWaitRoomKey(gameId)),
                String.valueOf(userId),
                String.valueOf(System.currentTimeMillis()));

        return Boolean.TRUE.equals(res);
    }

    @Override
    public boolean leaveWaitRoom(int gameId, long userId) {
        Long res = redisTemplate.opsForZSet().remove(getWaitRoomKey(gameId), String.valueOf(userId));

        return Objects.nonNull(res) && res > 0;
    }

    /**
     * 尝试预玩
     *
     * @param gameId 游戏 ID
     * @param userId 用户 ID
     * @return 结果
     */
    public PreplayResult tryPreplay(int gameId, long userId) {
        long roomId = SnowFlakeIdGenerator.generateId();
        Boolean res = redisTemplate.execute(preplayScript,
                Arrays.asList(getWaitRoomKey(gameId), getPrePlayRoomListKey(gameId), getGameRoomKey(gameId, roomId)),
                String.valueOf(roomId),
                String.valueOf(userId)
        );

        return Boolean.TRUE.equals(res) ? new PreplayResult(true, roomId) : new PreplayResult(false);
    }

    private String getWaitRoomKey(int gameId) {
        return String.format(WAIT_ROOM_KEY, gameId);
    }

    private String getPrePlayRoomListKey(int gameId) {
        return String.format(PRE_PLAY_ROOM_LIST_KEY, gameId);
    }

    private String getGameRoomKey(int gameId, long roomId) {
        return String.format(GAME_ROOM_KEY, gameId, roomId);
    }

    public static class PreplayResult {
        private final boolean success;

        private final Long roomId;

        public PreplayResult(boolean success) {
            this(success, null);
        }

        public PreplayResult(boolean success, Long roomId) {
            this.success = success;
            this.roomId = roomId;
        }

        public boolean isSuccess() {
            return success;
        }

        public Long getRoomId() {
            return roomId;
        }
    }
}
